package at.brandl.wwi;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import at.brandl.wwi.WhereWasIPoint.BoundaryEnum;

public class WhereWasIService extends Service {

	public static final int MAX_ACCURACY_METERS = 1000;
	private static final int MINUTE = 60 * 1000;
	private static final float MIN_DISTANCE_METERS = 100f;
	private static final int UPDATE_INTERVAL_MILLIS =  MINUTE;
	private WhereWasIDbAdapter dbHelper;

	private boolean created;
	private boolean started;
	private boolean recording;

	private LocationManager locationManager;
	private LocationUpdateListener locationListener;

	@Override
	public void onCreate() {
		// initialize db
		dbHelper = new WhereWasIDbAdapter(this);
		dbHelper.open();

		// initialize locationListener
		locationListener = new LocationUpdateListener();
		locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
		try {
			locationManager.requestLocationUpdates(
					LocationManager.NETWORK_PROVIDER, UPDATE_INTERVAL_MILLIS,
					MIN_DISTANCE_METERS, locationListener);
			locationManager.requestLocationUpdates(
					LocationManager.PASSIVE_PROVIDER, 0,
					MIN_DISTANCE_METERS, locationListener);
		} catch (IllegalArgumentException e) {
			Log.e(WhereWasIService.class.getName(),
					"could not request location updates", e);
		}

		// get first point
		WhereWasIPoint point = locationListener.getPoint(false);
		if (point != null) {
			point.setBoundary(BoundaryEnum.BEGIN);
			dbHelper.insertPoint(point);
		}

		recording = true;
		created = true;

		Notification notification = new Notification(R.drawable.android,
				getText(R.string.app_name), System.currentTimeMillis());
		Intent notificationIntent = new Intent(this, WhereWasIActivity.class);
		PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
				notificationIntent, 0);
		notification.setLatestEventInfo(this, getText(R.string.app_name),
				getText(R.string.app_name), pendingIntent);
		startForeground(1, notification);
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		started = true;
		return START_STICKY;
	}

	@Override
	public void onDestroy() {
		WhereWasIPoint point = locationListener.getPoint(false);
		if (point != null) {
			point.setBoundary(BoundaryEnum.END);
			dbHelper.insertPoint(point);
		}
		dbHelper.close();

		recording = false;
		created = false;
		started = false;
	}

	public boolean isCreated() {
		return created;
	}

	public boolean isStarted() {
		return started;
	}

	public boolean isRecording() {
		return recording;
	}

	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

	private class LocationUpdateListener implements LocationListener {

		private static final long TWO_MINUTES = 2 * MINUTE;
		private Location lastLocation;
		private WhereWasIPoint lastPoint;

		private WhereWasIPoint getPoint(boolean onlyDiffering) {
			WhereWasIPoint point = convertToPoint(getLastLocation());
			if (onlyDiffering
					&& (point == null || point.equalLocation(lastPoint))) {
				return null;
			}
			lastPoint = point;
			return point;
		}

		public void onLocationChanged(Location location) {
			location = filterLocation(location);
			Log.i(WhereWasIService.class.getName(), "comparing to " + lastLocation);
			if (isMoreExact(location) || hasMovedFarEnough(location)) {
				lastLocation = location;
				WhereWasIPoint point = getPoint(true);
				Log.i(WhereWasIService.class.getName(), "got point " + point);
				if (point != null) {
					dbHelper.insertPoint(point);
				}
			}
		}

		private Location getLastLocation() {
			if (lastLocation == null) {
				lastLocation = getLastKnownLocation();
			}
			return lastLocation;
		}

		private boolean hasMovedFarEnough(Location location) {
			if (location == null) {
				return false;
			}
			if (lastLocation == null) {
				return true;
			}
			return getDistance(lastLocation, location) > (MIN_DISTANCE_METERS
					+ lastLocation.getAccuracy() + location.getAccuracy());
		}

		private boolean isMoreExact(Location location) {
			if (location == null) {
				return false;
			}
			if (lastLocation == null) {
				return true;
			}
			return location.getAccuracy() < lastLocation.getAccuracy();
		}

		private float getDistance(Location lastLocation, Location location) {
			if (lastLocation == null) {
				return Float.MAX_VALUE;
			}
			float[] results = { 0 };
			Location.distanceBetween(lastLocation.getLatitude(),
					lastLocation.getLongitude(), location.getLatitude(),
					location.getLongitude(), results);
			return results[0];
		}

		private Location getLastKnownLocation() {
			Location resultLocation = null;
			Location netLocation = filterLocation(locationManager
					.getLastKnownLocation(LocationManager.NETWORK_PROVIDER));
			Location gpsLocation = filterLocation(locationManager
					.getLastKnownLocation(LocationManager.GPS_PROVIDER));
			if (gpsLocation == null) {
				resultLocation = netLocation;
			} else if (netLocation == null
					|| isBetter(gpsLocation, netLocation)) {
				resultLocation = gpsLocation;
			} else {
				resultLocation = netLocation;
			}
			Log.i(WhereWasIService.class.getName(),
					resultLocation == null ? "got no location" : "got "
							+ resultLocation);
			return resultLocation;
		}

		private boolean isBetter(Location location, Location otherLocation) {

			// Check whether the new location fix is newer or older
			long timeDelta = location.getTime() - otherLocation.getTime();
			boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
			boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
			boolean isNewer = timeDelta > 0;

			// If it's been more than two minutes since the current location,
			// use the new location
			// because the user has likely moved
			if (isSignificantlyNewer) {
				return true;
				// If the new location is more than two minutes older, it must
				// be worse
			} else if (isSignificantlyOlder) {
				return false;
			}

			// Check whether the new location fix is more or less accurate
			int accuracyDelta = (int) (location.getAccuracy() - otherLocation
					.getAccuracy());
			boolean isMoreAccurate = accuracyDelta < 0;
			boolean isSignificantlyLessAccurate = accuracyDelta > MIN_DISTANCE_METERS;

			// Determine location quality using a combination of timeliness and
			// accuracy
			if (isMoreAccurate) {
				return true;
			}
			if (isNewer && !isSignificantlyLessAccurate) {
				return true;
			}
			return false;

		}

		private Location filterLocation(Location location) {
			if (location == null || location.getAccuracy() > MAX_ACCURACY_METERS || location.getTime() < System.currentTimeMillis()
					- UPDATE_INTERVAL_MILLIS || location.getTime() > System.currentTimeMillis() + 2000) {
				return null;
			}
			return location;
		}

		private WhereWasIPoint convertToPoint(Location location) {
			if (location == null) {
				return null;
			}
			return new WhereWasIPoint(location.getTime(),
					location.getLongitude(), location.getLatitude(),
					location.getAccuracy());
		}

		public void onProviderDisabled(String provider) {
		}

		public void onProviderEnabled(String provider) {
		}

		public void onStatusChanged(String provider, int status, Bundle extras) {
		}

	}

}
